package core;

import java.util.ArrayList;
import java.util.List;
import net.ClientCommunicator;
import user.User;
import messages.*;
import event.*;
import exceptions.InvalidLoginParametersException;
import exceptions.UserAlreadyExistsException;
import java.io.IOException;
import commands.Command;
import commands.QuitCommand;

/**
 * Controller.
 * 
 * This singleton represents the kernel of the server. It is responsible to
 * handle every single request made by the clients (which it does through
 * the Command pattern).
 * 
 * Implements the ControllerObservable interface and notifies its observers
 * when events do occur.
 */
public class Controller implements ControllerObservable {
	
	/**
	 * Retrieves the single instance.
	 * This method is thread-safe because the instance is initialized
	 * when the program begins executing.
	 * 
	 * @return The instance.
	 */
	public static Controller getInstance() {
		return instance;
	}
	
	
	/**
	 * Private constructor to provide the semantics needed by the
	 * singleton pattern.
	 */
	private Controller() {
		this.userManager = new UserManager();		
		this.gameManager = new GameManager();
		this.messageManager = new MessageManager();
		this.pluginManager = new PluginManager();
	}
	
	
	public synchronized void subscribeEvents(ControllerObserver listener) {		
		this.listeners.add(listener);
	}
	
	
	public synchronized void unsubscribeEvents(ControllerObserver listener) {
		this.listeners.remove(listener);
	}
	
	
	/**
	 * Used by the client to authenticate himself.
	 * Returns an instance of User representing the client.
	 * 
	 * @param message The message containing login information provided by the client.
	 * @param communicator The communicator used to communicate with the client.
	 * @return An User representing the client.
	 */
	public User authenticate(LoginMessage message, ClientCommunicator communicator) {
		
		User user = null;
		try {
			
			synchronized(this.userManager) {
			
				System.err.println("Controller::authenticate - " + message);
				
				boolean wasLoggedIn = this.userManager.isLoggedIn(message.getLogin());
				
				// If the user is already logged in and he is not recovering an old session
				// then let's log him out first.
				if (wasLoggedIn && !message.isRetry()) {			
					Command command = new QuitCommand(new QuitMessage(), this.userManager.getUser(message.getLogin()));
					command.execute();				
				}
								
				user = this.userManager.authenticate(message.getLogin(), message.getPassword(), communicator);		
				if (message.isRetry() && wasLoggedIn) {
					try {
						UserLoggedInMessage loggedInMessage = new UserLoggedInMessage(user.getPlayer());
						loggedInMessage.setId(-1);
						loggedInMessage.setLastReceivedId(user.getLastMessageReceivedId());
						communicator.send(loggedInMessage);
					} catch (IOException e) {
						e.printStackTrace();
					}
					
					// The user is reconecting... We must send him the messages he didn't receive.
					messageManager.reSendAllAfter(user, message.getLastReceivedId());
				}
				
				if (!wasLoggedIn || !message.isRetry())
					this.fireUserLoggedInEvent(new UserLoggedInEventArgs(
							new UserLoggedInMessage(user.getPlayer())));
			}
		} catch (InvalidLoginParametersException e) {
			LoginFailedMessage replyMessage = new LoginFailedMessage(message);
			
			try {
				communicator.send(replyMessage);
			} catch (IOException e1) {
				System.out.println("Problem sending LoginFailed message to client!");
				this.forward(replyMessage, user);
				e1.printStackTrace();
			}
			
			this.fireLoginFailedEvent(new LoginFailedEventArgs(replyMessage));
		}
		return user;
	}
	
	
	/**
	 * Used by the client to register himself.
	 * Returns an instance of User representing the client.
	 * 
	 * @param message The message containing requested login/password.
	 * @param communicator The communicator used to communicate with the client.
	 * @return An User representing the client.
	 */
	public User register(RegisterMessage message, ClientCommunicator communicator) {
		
		User user = null;
		try {
			synchronized(this.userManager) {
				user = this.userManager.register(message.getLogin(), message.getPassword(), communicator);	
				
				this.fireRegistrationSuccessfulEvent(new RegistrationSuccessfulEventArgs(
						new RegistrationSuccessfulMessage(user.getPlayer().getLogin())));
				
				this.fireUserLoggedInEvent(new UserLoggedInEventArgs(
						new UserLoggedInMessage(user.getPlayer())));
			}
		} catch (UserAlreadyExistsException ex) {
			RegistrationFailedMessage replyMessage = new RegistrationFailedMessage(message.getLogin());
			
			try {
				communicator.send(replyMessage);
			} catch (IOException e) {
				this.forward(replyMessage, user);
				System.out.println("Problem sending RegistrationFailed message to client!");
				e.printStackTrace();
			}
			
			this.fireRegistrationFailedEvent(new RegistrationFailedEventArgs(replyMessage));
		}
		return user;
	}
	
	
	/**
	 * Used by the client-handlers to notify the controller about
	 * a new message that they received from the client.
	 * 
	 * Dispatches the message to a new Command and let's the command
	 * process the message.
	 * 
	 * @param message The message received.
	 */
	public void forward(Message message, User user) {
		
		if (message.getId() <= user.getLastMessageReceivedId()) {
			System.err.println("DUPLICATE MESSAGE: " + message + " from " + user.getLogin());
			return;
		}
		
		synchronized (messageManager) {
			// Deletes every message in the user queue that the user already received...
			messageManager.clearUserQueueUpTo(user, message.getLastReceivedId());

			// ...and re-sends messages the user didn't acknowledge yet.
			messageManager.reSendAllAfter(user, message.getLastReceivedId());
		}

		if (user.getLastMessageReceivedId() != message.getId() - 1 && !(message instanceof QuitMessage)) {
			System.err.println("Tou a receber as mensagens de " + user.getLogin() + " fora de ordem!");
		} else {		
			System.err.println("Processing " + message);
			
			user.setLastMessageReceivedId(message.getId());

			Command cmd = Command.parse(message, user);
			cmd.execute();
		}
	}
	
	
	/**
	 * Used by Watchdog and WatchdogListener classes.
	 * The kind of commands generated by those classes' messages, don't need any User instance.
	 * 
	 * Dispatches the message to a new Command and let's the command
	 * process the message.
	 * 
	 * @param message The message received.
	 */
	public void forward(Message message) {
		Command cmd = Command.parse(message, null);
		cmd.execute();
	}

	
	/**
	 * Returns the user manager - the entity responsible for managing
	 * known players and online clients.
	 * 
	 * Care must be taken when dealing with concurrent accesses to this object.
	 * 
	 * @see UserManager
	 * @return The user manager of this controler.
	 */
	public List<ControllerObserver> getListeners() {
		return this.listeners;
	}
	
	
	/**
	 * Returns the game manager - the entity responsible for managing
	 * current matches and pending invitations.
	 * 
	 * Care must be taken when dealing with concurrent accesses to this object.
	 * 
	 * @see GameManager
	 * @return The game manager of this controler.
	 */
	public GameManager getGameManager() {
		return this.gameManager;
	}
	
	
	/**
	 * Returns the message manager - the entity responsible for managing
	 * outgoing messages, including some error-handling.
	 * 
	 * Care must be taken when dealing with concurrent accesses to this object.
	 * 
	 * @see MessageManager
	 * @return The message manager of this controler.
	 */
	public MessageManager getMessageManager() {
		return this.messageManager;
	}
	
	
	public UserManager getUserManager() {
		return this.userManager;
	}
	
	
	public PluginManager getPluginManager() {
		return this.pluginManager;
	}	
	
	
	/**
	 * Fires the Wall event.
	 * 
	 * @param args Wall event arguments.
	 */
	public synchronized void fireWallEvent(WallEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.wallEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the Chat event.
	 * 
	 * @param args Chat event arguments.
	 */
	public synchronized void fireChatEvent(ChatEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.chatEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the ChatFailed event.
	 * 
	 * @param args ChatFailed event arguments.
	 */
	public synchronized void fireChatFailedEvent(ChatFailedEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.chatFailedEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the UserLoggedIn event.
	 * 
	 * @param args UserLoggedIn event arguments.
	 */
	public synchronized void fireUserLoggedInEvent(UserLoggedInEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.userLoggedInEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the UserLoggedOut event.
	 * 
	 * @param args UserLoggedOut event arguments.
	 */
	@SuppressWarnings("unchecked")
	public synchronized void fireUserLoggedOutEvent(UserLoggedOutEventArgs args) {
		// Must clone the listeners list because we will be modifying it.
		for (ControllerObserver listener : (List<ControllerObserver>) ((ArrayList) this.listeners).clone()) {
			listener.userLoggedOutEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the RegistrationSuccessful event.
	 * 
	 * @param args UserLoggedIn event arguments.
	 */
	public synchronized void fireRegistrationSuccessfulEvent(RegistrationSuccessfulEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.registrationSuccessfulEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the RegistrationFailed event.
	 * 
	 * @param args RegistrationFailed event arguments.
	 */
	public synchronized void fireRegistrationFailedEvent(RegistrationFailedEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.registrationFailedEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the InvitationReceived event.
	 * 
	 * @param args InvitationReceived event arguments.
	 */
	public synchronized void fireInvitationReceivedEvent(InvitationReceivedEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.invitationReceivedEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the InvalidInvitationReceived event.
	 * 
	 * @param args InvalidInvitationReceived event arguments.
	 */
	public synchronized void fireInvalidInvitationReceivedEvent(InvalidInvitationReceivedEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.invalidInvitationReceivedEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the InvitationTimedOut event.
	 * 
	 * @param args InvitationTimedOut event arguments.
	 */
	public synchronized void fireInvitationTimedOutEvent(InvitationTimedOutEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.invitationTimedOutEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the InvitationReply event.
	 * 
	 * @param args InvitationReply event arguments.
	 */
	public synchronized void fireInvitationReplyEvent(InvitationReplyEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.invitationReplyEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the InvalidInvitationReply event.
	 * 
	 * @param args InvalidInvitationReply event arguments.
	 */
	public synchronized void fireInvalidInvitationReplyEvent(InvalidInvitationReplyEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.invalidInvitationReplyEventHandler(args);
		}
	}	
	
	
	/**
	 * Fires the MatchStarted event.
	 * 
	 * @param args MatchStarted event arguments.
	 */
	public synchronized void fireMatchStartedEvent(MatchStartedEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.matchStartedEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the MatchEnded event.
	 * 
	 * @param args MatchEnded event arguments.
	 */
	public synchronized void fireMatchEndedEvent(MatchEndedEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.matchEndedEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the PlayerMove event.
	 * 
	 * @param args PlayerMove event arguments.
	 */
	public synchronized void firePlayerMoveEvent(PlayerMoveEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.playerMoveEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the InvalidMove event.
	 * 
	 * @param args InvalidMove event arguments.
	 */
	public synchronized void fireInvalidMoveEvent(InvalidMoveEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.invalidMoveEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the ListPlayers event.
	 * 
	 * @param args ListPlayers event arguments.
	 */
	public synchronized void fireListPlayersEvent(ListPlayersEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.listPlayersEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the LoginFailed event.
	 * 
	 * @param args LoginFailed event arguments.
	 */
	public synchronized void fireLoginFailedEvent(LoginFailedEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.loginFailedEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the ClientPing event.
	 * 
	 * @param args ClientPing event arguments.
	 */
	public synchronized void fireClientPingEvent(ClientPingEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.clientPingEventHandler(args);
		}
	}
		
	
	/**
	 * Fires the ListMatchs event.
	 * 
	 * @param args ListMatchs event arguments.
	 */
	public void fireListMatchsReplyEvent(ListMatchsReplyEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.listMatchsReplyEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the ListGames event.
	 * 
	 * @param args ListGames event arguments.
	 */
	public void fireListGamesReplyEvent(ListGamesReplyEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.listGamesReplyEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the InvalidUserDetailsRequest event.
	 * 
	 * @param args InvalidUserDetailsRequest event arguments.
	 */
	public void fireInvalidUserDetailsRequestEvent(InvalidUserDetailsRequestEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.invalidUserDetailsRequestEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the UserDetailsReply event.
	 * 
	 * @param args UserDetailsReply event arguments.
	 */
	public void fireUserDetailsReplyEvent(UserDetailsReplyEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.userDetailsReplyEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the AccountCreationSuccessful event.
	 * 
	 * @param args AccountCreationSuccessful event arguments.
	 */
	public void fireAccountCreationSuccessful(AccountCreationSuccessfulEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.accountCreationSuccessfulEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the AccountCreationFailed event.
	 * 
	 * @param args AccountCreationFailed event arguments.
	 */
	public void fireAccountCreationFailedEvent(AccountCreationFailedEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.accountCreationFailedEventHandler(args);
		}
	}


	/**
	 * Fires the AccountEditionSuccessful event.
	 * 
	 * @param args AccountEditionSuccessful event arguments.
	 */
	public void fireAccountEditionSuccessful(AccountEditionSuccessfulEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.accountEditionSuccessfulEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the AccountEditionFailed event.
	 * 
	 * @param args AccountEditionFailed event arguments.
	 */
	public void fireAccountEditionFailedEvent(AccountEditionFailedEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.accountEditionFailedEventHandler(args);
		}
	}

	
	/**
	 * Fires the AccountRemotionSuccessful event.
	 * 
	 * @param args AccountRemotionSuccessful event arguments.
	 */
	public void fireAccountRemotionSuccessful(AccountRemotionSuccessfulEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.accountRemotionSuccessfulEventHandler(args);
		}
	}


	/**
	 * Fires the AccountRemotionFailed event.
	 * 
	 * @param args AccountRemotionFailed event arguments.
	 */
	public void fireAccountRemotionFailedEvent(AccountRemotionFailedEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.accountRemotionFailedEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the ListPluginsReply event.
	 * 
	 * @param args ListPluginsReply event arguments.
	 */
	public void fireListPluginsReplyEvent(ListPluginsReplyEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.listPluginsReplyEventHandler(args);
		}
	}
	
	
	/**
	 * Fires the PluginOperationFailed event.
	 * 
	 * @param args PluginOperationFailed event arguments.
	 */
	public void firePluginOperationFailedEvent(PluginOperationFailedEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.pluginOperationFailedEventHandler(args);
		}
	}


	/**
	 * Fires the PluginOperationSuccessful event.
	 * 
	 * @param args PluginOperationSuccessful event arguments.
	 */
	public void firePluginOperationSuccessfulEvent(PluginOperationSuccessfulEventArgs args) {
		for (ControllerObserver listener : this.listeners) {
			listener.pluginOperationSuccessfulEventHandler(args);
		}
	}
		
	
	private List<ControllerObserver> listeners = new ArrayList<ControllerObserver>();
	private UserManager    userManager;		// Manages known players and online clients.
	private GameManager    gameManager;		// Manages current matches and pending invitations.
	private MessageManager messageManager;	// Manages outgoing messages.
	private PluginManager  pluginManager;	// Manages plugins
	
	
	private static final long serialVersionUID = 1L;
	private static Controller instance = new Controller(); // The singleton instance
}
